Passed
Pull Request — master (#6)
by Pawel
02:32
created

GraphHelpers.ts ➔ createSVGContainer   A

Complexity

Conditions 1

Size

Total Lines 13
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 12
dl 0
loc 13
rs 9.8
c 0
b 0
f 0
cc 1
1
import * as d3 from 'd3';
2
import { DependencyLink, DependencyNode } from '../../components/types';
3
import { Simulation } from 'd3';
4
5
export type GraphContainer = d3.Selection<SVGSVGElement, DependencyNode, Element, HTMLElement>;
6
7
export function generateLinkPath(this: Element, link: DependencyLink): void {
8
    if (!link.source.x || !link.source.y || !link.target.x || !link.target.y) {
9
        return;
10
    }
11
12
    const xDiff = link.source.x - link.target.x;
13
    const yDiff = link.source.y - link.target.y;
14
15
    const isSourceOnTheLeft = xDiff < 0;
16
    const isSourceBelowTarget = yDiff > 0;
17
18
    const angleInRadians = Math.abs(Math.atan(yDiff / xDiff));
19
    const cosinus = Math.cos(angleInRadians);
20
    const sinus = Math.sin(angleInRadians);
21
22
    const offsetXLeft = -50 * cosinus;
23
    const offsetY = 50 * sinus;
24
    const offsetYBelow = -offsetY - 5;
25
26
    const sourceLabelWidth = getNodeDimensions(link.source).width;
27
    const targetLabelWidth = getNodeDimensions(link.target).width;
28
29
    const sourceNewX = isSourceOnTheLeft ? (sourceLabelWidth + 20) * cosinus : offsetXLeft;
30
    const sourceNewY = isSourceBelowTarget ? offsetYBelow : offsetY;
31
32
    const targetNewX = isSourceOnTheLeft ? offsetXLeft : (targetLabelWidth + 20) * cosinus;
33
    const targetNewY = isSourceBelowTarget ? offsetY : offsetYBelow;
34
35
    d3.select<Element, DependencyLink>(this)
36
        .attr(
37
            'd',
38
            'M' +
39
                (link.source.x + sourceNewX) +
40
                ',' +
41
                (link.source.y + sourceNewY) +
42
                ', ' +
43
                (link.target.x + targetNewX) +
44
                ',' +
45
                (link.target.y + targetNewY)
46
        )
47
        .attr('stroke', 'black');
48
}
49
50
export function generateMarkers(svgContainer: GraphContainer): void {
51
    svgContainer
52
        .append('svg:defs')
53
        .append('svg:marker')
54
        .attr('id', 'provider')
55
        .attr('viewBox', '-5 -5 40 10')
56
        .attr('refX', 15)
57
        .attr('refY', 0)
58
        .attr('markerWidth', 40)
59
        .attr('markerHeight', 40)
60
        .attr('orient', 'auto')
61
        .append('svg:path')
62
        .attr('d', 'M0,-5L20,0L0,5,q10 -5,0 -10')
63
        .attr('fill', '#E5E5E6');
64
}
65
66
export function generateLabelPath(this: Node, node: DependencyNode) {
67
    const labelTextDimensions = getLabelTextDimensions(this);
68
69
    if (!labelTextDimensions) {
70
        return '';
71
    }
72
73
    const labelTextWidth = labelTextDimensions.width;
74
75
    const { isConsumer, isProvider } = node;
76
77
    if (isConsumer && isProvider) {
78
        return 'M4.5,35l9.37,14.59L4.5,64.18h' + (labelTextWidth + 45) + 'l9-14.59L' + (labelTextWidth + 49.5) + ',35H4.5z';
79
    }
80
81
    if (isProvider) {
82
        return 'M' + (labelTextWidth + 49.5) + ',35H4.5l9.37,14.59L4.5,64.18h' + (labelTextWidth + 45);
83
    }
84
85
    if (isConsumer) {
86
        return 'M4.5,64.18h' + (labelTextWidth + 45) + 'l9.42-14.59L' + (labelTextWidth + 49.5) + ',35H4.5';
87
    }
88
89
    return 'M4.5,64.18h' + (labelTextWidth + 55) + 'L' + (labelTextWidth + 59.5) + ',35H4.5';
90
}
91
92
export function handleDrag(simulation: Simulation<DependencyNode, DependencyLink>) {
93
    function dragStarted(node: DependencyNode) {
94
        if (!d3.event.active) {
95
            simulation.alphaTarget(0.3).restart();
96
        }
97
        node.fx = node.x;
98
        node.fy = node.y;
99
    }
100
101
    function dragged(node: DependencyNode) {
102
        node.fx = d3.event.x;
103
        node.fy = d3.event.y;
104
    }
105
106
    function dragEnded(node: DependencyNode) {
107
        if (!d3.event.active) {
108
            simulation.alphaTarget(0);
109
        }
110
        node.fx = null;
111
        node.fy = null;
112
    }
113
114
    return d3
115
        .drag<SVGGElement, DependencyNode>()
116
        .on('start', dragStarted)
117
        .on('drag', dragged)
118
        .on('end', dragEnded);
119
}
120
121
export function createSimulation(nodes: DependencyNode[], links: DependencyLink[], width: number, height: number) {
122
    return d3
123
        .forceSimulation(nodes)
124
        .force(
125
            'dependency',
126
            d3
127
                .forceLink<DependencyNode, DependencyLink>(links)
128
                .distance(180)
129
                .id((node: DependencyNode) => node.name)
130
        )
131
        .force('center', d3.forceCenter(width / 2, height / 2))
132
        .force('y', d3.forceY(0.5))
133
        .force('collide', d3.forceCollide(140))
134
        .force('nodeCollide', d3.forceCollide(140));
135
}
136
137
export function createSVGContainer(
138
    container: HTMLDivElement,
139
    width: number,
140
    height: number
141
): d3.Selection<SVGSVGElement, DependencyNode, Element, HTMLElement> {
142
    return d3
143
        .select<Element, DependencyNode>(`#${container.id}`)
144
        .append('svg')
145
        .attr('preserveAspectRatio', 'xMidYMid meet')
146
        .attr('viewBox', `0 0 ${width} ${height}`)
147
        .attr('width', width)
148
        .attr('height', height);
149
}
150
151
export function createZoom(svgContainer: GraphContainer): d3.Selection<SVGGElement, DependencyNode, Element, HTMLElement> {
152
    const zoomLayer = svgContainer.append('g');
153
154
    const zoomed = () => {
155
        zoomLayer.attr('transform', d3.event.transform);
156
    };
157
158
    svgContainer
159
        .call(
160
            d3
161
                .zoom<SVGSVGElement, DependencyNode>()
162
                .scaleExtent([1 / 2, 12])
163
                .on('zoom', zoomed)
164
        )
165
        .on('dblclick.zoom', null);
166
167
    return zoomLayer;
168
}
169
170
export function getLabelTextDimensions(node: Node) {
171
    const textNode = d3.select<SVGGElement, DependencyNode>(node.previousSibling as SVGGElement).node();
172
173
    if (!textNode) {
174
        return undefined;
175
    }
176
177
    return textNode.getBBox();
178
}
179
180
export function getNodeDimensions(selectedNode: DependencyNode): { width: number; height: number } {
181
    const foundNode = d3
182
        .select<SVGGElement, DependencyNode>('#labels')
183
        .selectAll<SVGGElement, DependencyNode>('g')
184
        .filter((node: DependencyNode) => node.x === selectedNode.x && node.y === selectedNode.y)
185
        .node();
186
    return foundNode ? foundNode.getBBox() : { width: 200, height: 25 };
187
}
188